前幾天我們已經完成 RAG 的前置工作:
目前系統已經能夠根據使用者的問題,從知識庫中找到相關內容
但是,還有一個重要問題:
搜尋到的資料,如何交給 LLM,讓它理解內容並產生自然的回答
如果只使用 Vector Search,我們得到的通常是幾段文字,而不是完整的對話答案
因此,今天要把 Vector Search 與 LLM 串接起來
完成第一個可以根據知識庫回答問題的 RAG Assistant
假設知識庫中有以下內容:
本產品支援 Windows 10、Windows 11 以及 Ubuntu 22.04 與 Ubuntu 24.04
使用者可以透過官方網站下載安裝程式
安裝完成後,需要重新啟動系統
當使用者提問:
這個產品支援哪些作業系統
系統應該執行以下流程:
最終回答可能是:
本產品支援 Windows 10、Windows 11、Ubuntu 22.04 以及 Ubuntu 24.04
這就是 RAG 的核心概念:
先從知識庫取得相關資訊,再讓 LLM 根據這些資訊產生回答
前幾天我們將 RAG 拆成兩個階段:
這個階段主要負責建立可搜尋的知識庫
其中,今天的重點是:
Relevant Chunks → Prompt → LLM → Final Answer
RAG 主要由兩個部分組成
負責:
知識庫中有哪些內容可能與使用者問題有關
例如:
使用者問題:
這個產品支援哪些作業系統
搜尋結果:
本產品支援 Windows 10、Windows 11 以及 Ubuntu 22.04 與 Ubuntu 24.04
負責:
如何根據搜尋結果,產生使用者看得懂的回答
例如:
本產品支援 Windows 10、Windows 11、Ubuntu 22.04 以及 Ubuntu 24.04
需要注意的是:
Vector Search 不會直接產生完整答案,LLM 也不應該在沒有參考資料的情況下隨意補充資訊
本次實作延續 Day 5 的技術:
使用 Gemini API 作為示範
實際使用哪一種 LLM,並不會改變 RAG 的核心流程
ChromaDB、Embedding 模型與 Gemini SDK:
pip install chromadb sentence-transformers langchain-text-splitters google-genai python-dotenv
在專案根目錄建立 .env
API Key 應該透過環境變數讀取
GEMINI_API_KEY=your_api_key
import os
from dotenv import load_dotenv
from google import genai
load_dotenv()
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
raise ValueError("找不到 GEMINI_API_KEY")
client = genai.Client(api_key=api_key)
這裡先建立 Gemini Client,後續可以透過它呼叫模型
實際使用的模型名稱,應依照目前 API 可用模型與帳號權限進行設定
from pathlib import Path
import chromadb
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
)
from sentence_transformers import SentenceTransformer
DATA_PATH = Path("data/sample.txt")
CHROMA_PATH = "./data/chroma"
def create_collection():
client = chromadb.PersistentClient(path=CHROMA_PATH)
collection = client.get_or_create_collection(
name="knowledge_base"
)
return collection
def load_chunks():
text = DATA_PATH.read_text(encoding="utf-8")
splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20
)
chunks = splitter.split_text(text)
return chunks
def build_vector_database():
collection = create_collection()
chunks = load_chunks()
model = SentenceTransformer(
"all-MiniLM-L6-v2"
)
embeddings = model.encode(chunks)
collection.upsert(
ids=[
f"chunk-{i}"
for i in range(len(chunks))
],
documents=chunks,
embeddings=embeddings.tolist(),
metadatas=[
{"source": "sample.txt"}
for _ in chunks
]
)
return collection
def search_documents(query, n_results=2):
collection = create_collection()
model = SentenceTransformer(
"all-MiniLM-L6-v2"
)
query_embedding = model.encode([query])
results = collection.query(
query_embeddings=query_embedding.tolist(),
n_results=n_results
)
documents = results.get("documents", [[]])[0]
return documents
測試搜尋:
if __name__ == "__main__":
build_vector_database()
query = "這個產品支援哪些作業系統"
documents = search_documents(query)
for i, document in enumerate(documents):
print(f"\n結果 {i + 1}")
print(document)
可能得到類似結果:
結果 1
本產品支援 Windows 10、Windows 11 以及 Ubuntu 22.04 與 Ubuntu 24.04
搜尋結果會依照向量相似度排序
不過,搜尋結果只是文字片段,還需要交給 LLM 進行理解與組織
LLM 不一定需要知道 Vector Database 的內部結構
我們只需要將搜尋到的文件內容整理成一段 Context
def build_context(documents):
context = "\n\n".join(documents)
return context
例如:
documents = [
"本產品支援 Windows 10、Windows 11",
"本產品支援 Ubuntu 22.04 與 Ubuntu 24.04"
]
context = build_context(documents)
print(context)
輸出:
本產品支援 Windows 10、Windows 11
本產品支援 Ubuntu 22.04 與 Ubuntu 24.04
Context 就是接下來提供給 LLM 參考的資料。
建立一個基本的 Prompt Template:
def build_prompt(query, context):
prompt = f"""
你是一個專業的 AI 智慧助理
請根據下方提供的參考資料回答使用者問題
回答規則:
1. 只能根據參考資料回答
2. 如果參考資料沒有相關資訊,請明確說明目前資料不足
3. 不要自行捏造文件中沒有提到的資訊
4. 使用繁體中文回答
5. 回答要清楚、簡潔
參考資料:
{context}
使用者問題:
{query}
請提供回答:
"""
return prompt
這裡的 Prompt 主要包含三個部分:
from src.llm.client import client
from src.rag.vector_db import search_documents
from src.rag.vector_db import build_vector_database
MODEL_NAME = "gemini-2.5-flash"
def build_context(documents):
return "\n\n".join(documents)
def build_prompt(query, context):
return f"""
你是一個專業的 AI 智慧助理。
請根據下方提供的參考資料回答使用者問題。
回答規則:
1. 只能根據參考資料回答。
2. 如果資料不足,請明確說明。
3. 不要捏造參考資料沒有提到的內容。
4. 使用繁體中文回答。
參考資料:
{context}
使用者問題:
{query}
請提供回答:
"""
def ask_rag(query):
documents = search_documents(
query=query,
n_results=2
)
context = build_context(documents)
prompt = build_prompt(
query=query,
context=context
)
response = client.models.generate_content(
model=MODEL_NAME,
contents=prompt
)
return {
"answer": response.text,
"documents": documents
}
if __name__ == "__main__":
build_vector_database()
query = "這個產品支援哪些作業系統"
result = ask_rag(query)
print("回答:")
print(result["answer"])
print("\n參考文件:")
for document in result["documents"]:
print(document)
在實際結果仍可能受到以下因素影響:
因此,Prompt 指示不能取代完整的驗證機制
即使 Prompt 要求模型只能根據文件回答,LLM 仍可能使用自身訓練知識補充內容
這可能造成錯誤資訊
如果 Chunking 或 Embedding 效果不佳,Vector Search 可能找不到正確資料
這時候 LLM 即使能力很強,也沒有足夠的參考資料回答問題
RAG 的品質不只取決於 LLM,也取決於前面的 Retrieval
如果搜尋結果包含太多文字,可能造成:
如果每次執行程式都新增相同資料,可能產生重複內容
為每個 Chunk 建立固定 ID
不過在正式系統中,仍應該根據文件版本、內容雜湊值或唯一識別碼設計更完整的資料管理方式
今天完成了第一個基本的 RAG Pipeline,將前幾天的功能串接起來
重要學習內容包括:
今天最大的收穫是:
RAG 不只是把文件丟給 LLM,而是透過搜尋、資料組合與提示設計,建立一個有依據的回答流程
我們已經從單純的 LLM API 呼叫,進一步建立了具備外部知識檢索能力的 AI 助理
但目前仍然存在一些限制:
今天的 RAG Assistant 已經可以根據知識庫回答問題
下一步,我們要進一步思考:
如果使用者想知道答案來自哪一份文件,系統能不能提供引用來源
因此,Day 7 將會探討:
從「能夠回答問題」,進一步走向「能夠說明答案依據」